package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"../util"
	log "github.com/kpango/glg"
	"gopkg.in/yaml.v2"
)

//// HTTP api
func (rl *RuleList) CheckAvailable(w http.ResponseWriter, r *http.Request) {
	latnId := r.FormValue("latnId")
	if latnId == "" {
		w.WriteHeader(404)
		w.Write([]byte("request parameters error\n"))
		return
	}
	if rl.Filter(latnId) {
		w.WriteHeader(200)
		w.Write([]byte("server available\n"))
	} else {
		w.WriteHeader(404)
		w.Write([]byte("server not available"))
	}
}

func (rl *RuleList) Disable(w http.ResponseWriter, r *http.Request) {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	timestamp := r.FormValue("timestamp")
	if timestamp == "" {
		w.WriteHeader(404)
		w.Write([]byte("request parameters error\n"))
		return
	}
	if rule, ok := rl.Temporary[timestamp]; ok {
		log.Infof("disable temp rule %s: %s", timestamp, rule.String())
		delete(rl.Temporary, timestamp)
	}
	w.WriteHeader(200)
	w.Write([]byte("disable rule success"))
}

func (rl *RuleList) AddRule(w http.ResponseWriter, r *http.Request) {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	if r.FormValue("latnId") == "" ||
		r.FormValue("allow") == "" ||
		r.FormValue("startDate") == "" ||
		r.FormValue("startTime") == "" ||
		r.FormValue("endDate") == "" ||
		r.FormValue("endTime") == "" ||
		r.FormValue("due") == "" {
		w.WriteHeader(404)
		w.Write([]byte("request parameters error\n"))
		return
	}
	var allow bool
	if r.FormValue("allow") == "true" {
		allow = true
	} else {
		allow = false
	}
	startDate, err := strconv.Atoi(r.FormValue("startDate"))
	if err != nil {
		w.WriteHeader(404)
		desc := fmt.Sprintf("parse rule error: %v", err)
		w.Write([]byte(desc))
		return
	}
	endDate, err := strconv.Atoi(r.FormValue("endDate"))
	if err != nil {
		w.WriteHeader(404)
		desc := fmt.Sprintf("parse rule error: %v", err)
		w.Write([]byte(desc))
		return
	}
	rule := Rule{
		LatnId:    r.FormValue("latnId"),
		Allow:     allow,
		StartTime: r.FormValue("startTime"),
		StartDate: startDate,
		EndTime:   r.FormValue("endTime"),
		EndDate:   endDate,
		Due:       r.FormValue("due"),
	}
	if err := parseRules(&rule, allow); err != nil {
		w.WriteHeader(404)
		desc := fmt.Sprintf("parse rule error: %v", err)
		w.Write([]byte(desc))
		return
	}
	timestamp := time.Now().Format("20060102150405")
	rl.Temporary[timestamp] = rule
	log.Infof("add temp rule: %s", rule.String())
	w.WriteHeader(200)
	w.Write([]byte("add rule success"))

}

func (rl *RuleList) GetRules(w http.ResponseWriter, r *http.Request) {
	rules := rl.GetRelatedRules(r.FormValue("latnId"))
	resp := "\nTemp:\n"
	for t, rule := range rules.Temporary {
		resp = resp + t + ": " + rule.String() + "\n"
	}
	resp = resp + "\nAllow:\n"
	for _, rule := range rules.Allow {
		resp = resp + rule.String() + "\n"
	}
	resp = resp + "\nNotAllow:\n"
	for _, rule := range rules.NotAllow {
		resp = resp + rule.String() + "\n"
	}
	w.WriteHeader(200)
	w.Write([]byte(resp))
}

//// RuleList & Rule function
type RuleList struct {
	Allow     []Rule          `yaml:"Allow"`
	NotAllow  []Rule          `yaml:"NotAllow"`
	Temporary map[string]Rule // timestamp:Rule
	watcher   *util.FileWatcher
	mu        *sync.RWMutex
}

type Rule struct {
	LatnId    string `yaml:"latnId"`
	Allow     bool   `yaml:"allow"`
	StartDate int    `yaml:"start_date"`
	StartTime string `yaml:"start_time"`
	EndDate   int    `yaml:"end_date"`
	EndTime   string `yaml:"end_time"`
	Due       string
	due       time.Time
}

func NewRules(fileName string) (*RuleList, error) {
	rules := &RuleList{
		Allow:     make([]Rule, 0),
		NotAllow:  make([]Rule, 0),
		Temporary: make(map[string]Rule), // timestamp:Rule
		mu:        new(sync.RWMutex),
		watcher:   new(util.FileWatcher),
	}

	watcher, err := util.NewWatcher()
	if err != nil {
		return rules, fmt.Errorf("RuleList.NewWatcher: %v", err)
	}
	rules.watcher = watcher

	err = rules.LoadRules(fileName)
	if err != nil {
		return rules, err
	}

	return rules, nil
}

/// Rule func
func (r *Rule) String() string {
	allow := "Allow"
	if r.Allow == false {
		allow = "NotAllow"
	}
	due := r.Due
	if r.due.IsZero() {
		due = "forever"
	}
	return fmt.Sprintf("{Type: \"%s\", LatnId: \"%s\", Interval: \"%d号%s ~ %d号%s\", Due: \"%s\"}",
		allow, r.LatnId, r.StartDate, r.StartTime, r.EndDate, r.EndTime, due)
}

// return if rule is due
func (r *Rule) Valid() bool {
	if r.Due == "" {
		return false
	}
	return time.Now().Before(r.due)
}

// return if rule is related to given latnId
func (r *Rule) IsRelated(latnId string) bool {
	if r.LatnId == "all" {
		return true
	}
	return strings.Contains(r.LatnId, latnId)
}

// return if server is available according to rule type and time interval
// allow && in interval: true
// not allow && not in interval: true
// -> is allow == is in interval
func (r *Rule) PassFilter() bool {
	return r.Allow == r.InInterval()
}

// return if Now is in time interval of rule
func (r *Rule) InInterval() bool {
	now := time.Now()
	local, _ := time.LoadLocation("Local")
	if now.Day() < r.StartDate || now.Day() > r.EndDate {
		return false
	}
	if now.Day() > r.StartDate || now.Day() < r.EndDate {
		return true
	}
	if now.Day() == r.StartDate {
		s := fmt.Sprintf("%d.%d.%d %s", now.Year(), now.Month(), r.StartDate, r.StartTime)
		start, _ := time.ParseInLocation("2006.1.2 15:04:05", s, local)
		if now.Before(start) {
			return false
		}
	}
	if now.Day() == r.EndDate {
		s := fmt.Sprintf("%d.%d.%d %s", now.Year(), now.Month(), r.EndDate, r.EndTime)
		end, _ := time.ParseInLocation("2006.1.2 15:04:05", s, local)
		if now.After(end) {
			return false
		}
	}
	return true
}

/// RuleList func
func (rl *RuleList) LoadRules(fileName string) error {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	var rules RuleList
	content, err := ioutil.ReadFile(fileName)
	if err != nil {
		return fmt.Errorf("RuleList.LoadRules.ReadFile: %v", err)
	}

	err = yaml.Unmarshal(content, &rules)
	if err != nil {
		return fmt.Errorf("RuleList.LoadRules.ParseYaml: %v", err)
	}

	rl.Allow = rules.Allow
	rl.NotAllow = rules.NotAllow
	err = rl.Parse()
	if err != nil {
		return err
	}
	log.Infof("load rules success.")
	return nil
}

func (rl *RuleList) ReloadIfModified(fileName string) {
	rl.watcher.Watch(fileName, func(fileName string) {
		err := rl.LoadRules(fileName)
		if err != nil {
			log.Errorf("[Error] %v", err)
		}
	})
}

func (rl *RuleList) CloseWatcher() {
	rl.watcher.Close()
}

func (rl *RuleList) Parse() error {
	for i, rule := range rl.Allow {
		err := parseRules(&rule, true)
		if err != nil {
			return err
		}
		rl.Allow[i] = rule
	}
	for i, rule := range rl.NotAllow {
		err := parseRules(&rule, false)
		if err != nil {
			return err
		}
		rl.NotAllow[i] = rule
	}
	return nil
}

func parseRules(rule *Rule, allow bool) error {
	rule.Allow = allow
	now := time.Now()
	local, err := time.LoadLocation("Local")
	if err != nil {
		return fmt.Errorf("parse time error: %v", err)
	}
	s := fmt.Sprintf("%d.%d.%d %s", now.Year(), now.Month(), rule.StartDate, rule.StartTime)
	_, err = time.ParseInLocation("2006.1.2 15:04:05", s, local)
	if err != nil {
		return fmt.Errorf("parse rule error: %s. error: %v", rule.String(), err)
	}
	s = fmt.Sprintf("%d.%d.%d %s", now.Year(), now.Month(), rule.EndDate, rule.EndTime)
	_, err = time.ParseInLocation("2006.1.2 15:04:05", s, local)
	if err != nil {
		return fmt.Errorf("parse rule error: %s. error: %v", rule.String(), err)
	}

	if rule.Due == "" {
		return nil
	}
	due, err := time.ParseInLocation("20060102150405", rule.Due, local)
	if err != nil {
		return fmt.Errorf("parse rule error: %s. error: %v", rule.String(), err)
	}
	rule.due = due
	return nil
}

func (rl *RuleList) GetRelatedRules(latnId string) RuleList {
	// check if temp rule is due
	dueRule := make([]string, 0)
	for t, rule := range rl.Temporary {
		// check if rule is due
		if !rule.Valid() {
			log.Infof("rule due: %s", rule.String())
			dueRule = append(dueRule, t)
			continue
		}
	}
	rl.mu.Lock()
	defer rl.mu.Unlock()
	// delete due rule
	for _, rule := range dueRule {
		delete(rl.Temporary, rule)
	}

	result := RuleList{
		Allow:     make([]Rule, 0),
		NotAllow:  make([]Rule, 0),
		Temporary: make(map[string]Rule),
	}
	if latnId == "" || latnId == "all" {
		return *rl
	}
	for _, rule := range rl.Allow {
		if rule.IsRelated(latnId) {
			result.Allow = append(result.Allow, rule)
		}
	}
	for _, rule := range rl.NotAllow {
		if rule.IsRelated(latnId) {
			result.NotAllow = append(result.NotAllow, rule)
		}
	}
	for t, rule := range rl.Temporary {
		if rule.IsRelated(latnId) {
			result.Temporary[t] = rule
		}
	}
	return result
}

// Temp rule: have higher priority than Allow and NotAllow rules.
// available if there is one related Allow rules,
//   or not avaiable if there is one related NotAllow rules,
//   so make sure there are no conflict rules.
// related if latnId and time match
func (rl *RuleList) FilterByTempRules(latnId string) (related bool, available bool) {
	dueRule := make([]string, 0)
	available = true
	related = false
	for t, rule := range rl.Temporary {
		// check if rule is due
		if !rule.Valid() {
			log.Infof("rule due: %s\n", rule.String())
			dueRule = append(dueRule, t)
			continue
		}
		// check if rule is related
		if !rule.IsRelated(latnId) {
			continue
		}
		log.Infof("find temporary rule of latnId %s: %s.\n", latnId, rule.String())
		if !rule.Allow && !rule.PassFilter() {
			related = true
			available = false
			break
		}
		if rule.Allow && rule.PassFilter() {
			related = true
			break
		}
	}
	if !related {
		log.Infof("no related temporary rule of latnId %s, available: true", latnId)
	} else {
		log.Infof("exist related temporary rule of latnId %s, available: %v.\n",
			latnId, available)
	}

	rl.mu.Lock()
	defer rl.mu.Unlock()
	// delete due rule
	for _, rule := range dueRule {
		delete(rl.Temporary, rule)
	}
	return related, available
}

// Allow rules: available if No related rules or pass one of related rules.
// Not available only if there are related rules but satisfy none.
func (rl *RuleList) FilterByAllowRules(latnId string) bool {
	related := false
	for _, rule := range rl.Allow {
		if rule.IsRelated(latnId) {
			related = true
			log.Infof("find related Allow rule of latnId %s : %s.", latnId, rule.String())
			if rule.PassFilter() {
				log.Infof("satisfy one Allow rule of latnId %s: %s. available: true.", latnId, rule.String())
				return true
			}
		}
	}
	if related {
		log.Infof("find related Allow rules of latnId %s, but satisfy none. available: false.", latnId)
	} else {
		log.Infof("no related Allow rules of latnId %s, available: true.", latnId)
	}
	return !related
}

// NotAllow rules: not available if satisfy one related rules.
func (rl *RuleList) FilterByNotAllowRules(latnId string) bool {
	for _, rule := range rl.NotAllow {
		if rule.IsRelated(latnId) && !rule.PassFilter() {
			log.Infof("find related NotAllow rule of latnId %s : %s. available: false.", latnId, rule.String())
			return false
		}
	}
	log.Infof("pass all NotAllow rules. available: true")
	return true
}

// Temp rules have higher priority.
// If no related Temp rules, Allow rules and NotAllow rules will be considered.
// Avaiable if pass Allow rules and no related NotAllow rules
func (rl *RuleList) Filter(latnId string) bool {
	related, available := rl.FilterByTempRules(latnId)
	if related {
		return available
	}
	available = rl.FilterByAllowRules(latnId) && rl.FilterByNotAllowRules(latnId)
	log.Infof("check server available: latnId:%s, available: %v", latnId, available)
	return available
}
